【GUI】入门 Noise(二):深度解析:Racket 嵌入 Swift 应用的底层机制

#lisp/racket #gui/noise

引言

Noise 是一个 Swift 包装器,用于简化在 Swift 应用中嵌入 Racket CS 运行时的复杂过程。乍一看,它似乎只是简单地"包装了一个 Racket 库",但实际上,整个集成过程涉及多个层次和组件,理解其工作原理需要深入分析编译和运行时两个阶段。

本文将深入探讨 Noise 的核心架构,解答以下关键问题:

架构概览:三层分离的设计

Noise 采用三层分离的架构设计:

┌─────────────────────────────────────────┐
│           你的 Swift 应用                │
└────────────┬────────────────────────────┘
             │
┌────────────▼────────────────────────────┐
│      Noise 框架 (Swift 代码)             │
│  - Racket.swift (运行时封装)            │
│  - Serde.swift (序列化)                  │
│  - Backend.swift (RPC)                  │
└────────────┬────────────────────────────┘
             │
┌────────────▼────────────────────────────┐
│   RacketCS.xcframework (静态库)         │
│  - libracketcs.a (Racket VM)            │
│  - racket.h, racketcs.h (头文件)        │
└─────────────────────────────────────────┘

核心理解:每一层都有独立的职责,彼此之间通过明确定义的接口通信。

一、Racket CS 的最小运行时包含哪些文件?

Noise 需要从 Racket 中提取三类文件:

1. 静态库文件 (.a)

每个平台对应的 Racket CS 编译产物:

Lib/
├── libracketcs-arm64-ios.a              # iOS 设备架构
├── libracketcs-arm64-iphonesimulator.a  # iOS 模拟器架构
├── libracketcs-arm64-macos.a            # macOS ARM64 架构
└── libracketcs-x86_64-macos.a           # macOS Intel 架构

这些静态库被封装成 .xcframework

RacketCS-macos.xcframework: Lib/include/* Lib/libracketcs-universal-macos.a
    xcodebuild -create-xcframework \
        -library Lib/libracketcs-universal-macos.a \
        -headers Lib/include \
        -output $@

静态库包含:Chez Scheme 虚拟机、Racket CS 核心运行时、C API 函数实现

2. 头文件 (.h)

Lib/include/
├── racket.h              # 主入口,根据平台包含对应的 chezscheme.h
├── racketcs.h            # Racket CS API 定义
├── racketcsboot.h        # 启动参数结构定义
├── chezscheme-arm64-macos.h      # macOS ARM64 的 Chez Scheme 接口
├── chezscheme-x86_64-macos.h     # macOS Intel 的 Chez Scheme 接口
├── chezscheme-arm64-ios.h        # iOS 设备的 Chez Scheme 接口
└── chezscheme-arm64-iphonesimulator.h  # iOS 模拟器的 Chez Scheme 接口

头文件包含:C 函数声明、数据结构定义、宏定义

3. Boot 文件

Sources/NoiseBoot_iOS/boot/arm64-ios/
├── petite.boot    # Petite Scheme 基础镜像
├── scheme.boot    # 完整的 Scheme 镜像
└── racket.boot    # Racket 语言扩展镜像

Sources/NoiseBoot_macOS/boot/arm64-macos/
├── petite.boot
├── scheme.boot
└── racket.boot

Boot 文件包含:编译后的字节码、初始数据结构、核心库代码

二、编译流程详解

阶段 1:Racket 源码 → 静态库

这是在构建 Racket CS 时完成的,与 Noise 无关:

Racket 源码 (C/Scheme)
    ↓ 配置和编译 (configure & make)
libracketcs.a (静态库)
    ↓ xcodebuild 打包
RacketCS.xcframework (binary target)

阶段 2:Swift 项目的编译

看 Package.swift 的定义:

.binaryTarget(
    name: "RacketCS-ios",
    path: "RacketCS-ios.xcframework"
),
.binaryTarget(
    name: "RacketCS-macos",
    path: "RacketCS-macos.xcframework"
),
.target(
    name: "Noise",
    dependencies: [
        .target(name: "NoiseBoot_iOS", condition: .when(platforms: [.iOS])),
        .target(name: "NoiseBoot_macOS", condition: .when(platforms: [.macOS])),
        .target(name: "RacketCS-ios", condition: .when(platforms: [.iOS])),
        .target(name: "RacketCS-macos", condition: .when(platforms: [.macOS])),
    ],
    linkerSettings: [
        .linkedLibrary("curses", .when(platforms: [.macOS])),
        .linkedLibrary("iconv"),
    ]
)

编译顺序

  1. 预编译阶段(已存在于仓库中):

    • RacketCS-ios.xcframework
    • RacketCS-macos.xcframework
  2. 编译阶段(Swift 编译器处理):

    NoiseBoot_iOS (只有资源)
    NoiseBoot_macOS (只有资源)
    Noise (Swift 代码,依赖上述)
    NoiseBackend (Swift 代码,依赖 Noise)
    NoiseSerde (Swift 代码)
    你的应用 (依赖上述所有)
    
  3. 链接阶段

    • Swift 调用 Clang 模块导入器
    • 读取 RacketCS.xcframework 中的头文件
    • 链接静态库中的符号

关键点import RacketCS 在 Racket.swift 并不是导入一个 Swift 模块,而是告诉 Swift Package Manager 链接对应的 binaryTarget。

三、头文件被谁使用?如何使用?

当你编写 import RacketCS 时,Swift 编译器会:

  1. 找到对应的 binaryTargetRacketCS-macosRacketCS-ios
  2. 读取头文件:从 xcframeworkHeaders/ 目录
  3. 解析头文件:使用 Clang 的模块映射功能

头文件的组织

racket.h 是主入口(见 racket.h):

#if defined(__x86_64__)
# include "chezscheme-x86_64-macos.h"
#elif defined(__arm64__)
# include "TargetConditionals.h"
# if TARGET_OS_IPHONE
#  define _Nonnull
#  define _Nullable
#  include "chezscheme-arm64-ios.h"
# else
#  include "chezscheme-arm64-macos.h"
# endif
#endif

#include "racketcs.h"

根据编译目标自动选择正确的平台头文件。

Swift 如何调用 C 函数

Swift 编译器通过头文件了解 C 函数的签名,例如 racketcs.h 中的定义(racketcs.h):

RACKET_API_EXTERN ptr racket_apply(ptr proc, ptr arg_list);
RACKET_API_EXTERN ptr racket_primitive(const char *name);
RACKET_API_EXTERN void racket_embedded_load_file(const char *path, int as_predefined);

Swift 会自动将这些函数映射为 Swift 函数调用,可以直接在 Swift 代码中使用。

总结:头文件是 Swift 和 C 之间的"语言桥梁",定义了双方都能理解的接口。

四、谁来加载 Boot 文件?何时加载?

Boot 文件的加载过程

Boot 文件不是在编译时加载,而是在运行时由 Swift 代码加载。

看 Racket.swift:

public init(execPath: String = "racket") {
    var args = racket_boot_arguments_t()
    args.exec_file = execPath.utf8CString.cstring()
    
    // 获取 boot 文件路径
    args.boot1_path = NoiseBoot.petiteURL.path.utf8CString.cstring()
    args.boot2_path = NoiseBoot.schemeURL.path.utf8CString.cstring()
    args.boot3_path = NoiseBoot.racketURL.path.utf8CString.cstring()
    
    // 调用 C 函数初始化 Racket VM
    racket_boot(&args)
    racket_deactivate_thread()
    
    // 清理
    args.exec_file.deallocate()
    args.boot1_path.deallocate()
    args.boot2_path.deallocate()
    args.boot3_path.deallocate()
}

Boot 文件路径的获取

在 NoiseBoot.swift 中:

public struct NoiseBoot {
  public static let petiteURL = Bundle.module.url(forResource: "boot/\(ARCH)-macos/petite", withExtension: "boot")!
  public static let schemeURL = Bundle.module.url(forResource: "boot/\(ARCH)-macos/scheme", withExtension: "boot")!
  public static let racketURL = Bundle.module.url(forResource: "boot/\(ARCH)-macos/racket", withExtension: "boot")!
}

这些 boot 文件在编译时通过 .copy("boot") 作为资源复制到应用 bundle 中。

racket_boot() 做了什么?

racket_boot() 是 C 函数,在静态库 libracketcs.a 中实现:

  1. 读取 petite.boot,初始化 Petite Scheme
  2. 读取 scheme.boot,扩展为完整 Scheme
  3. 读取 racket.boot,加载 Racket 语言特性
  4. 设置参数(如 exec_file, argv 等)
  5. 启动 Racket VM,进入可运行状态

关键理解:boot 文件是 Racket VM 的"启动镜像",包含了解释器和基础库的字节码。没有它们,Racket VM 就像一个空壳,无法执行任何代码。

五、运行流程详解

完整的启动流程

1. Swift 应用启动
   ↓
2. 创建 Racket 实例
   let racket = Racket()
   ↓
3. Racket.init() 执行:
   - 获取 boot 文件路径
   - 调用 racket_boot(&args)
   - racket_boot() 读取三个 boot 文件
   - 初始化 Racket VM
   ↓
4. 加载 Racket 代码
   racket.load(zo: URL(fileURLWithPath: "/path/to/noise-serde.zo"))
   ↓ 调用 racket_embedded_load_file()
   - 编译 Racket 代码到内存
   - 注册模块和函数
   ↓
5. 使用 Racket 功能
   racket.require("noise/serde", from: "module")
   - 调用 racket_dynamic_require()
   - 加载指定的模块
   - 返回模块的导出对象
   ↓
6. Swift 和 Racket 交互
   - Swift → Racket: 调用 racket_apply()
   - Racket → Swift: 使用 NoiseSerde 序列化

值包装和垃圾回收

Swift 通过 Val 类型包装 Racket 的指针(见 Racket.swift):

struct Val {
    let ptr: ptr  // void* 指针,指向 Racket 堆中的对象
}

关键机制

六、noise-serde-lib 的双重角色

这是最容易误解的部分。很多人认为 noise-serde-lib 只是一个代码生成器,但实际上它有两个角色:

角色 1:编译时代码生成器

codegen.rkt 将 Racket 的类型定义转换为 Swift 代码:

#lang racket/base

(define-record person
  [name : String]
  [age : Int32])

运行 racket -l noise/codegen 生成:

public struct Person: Readable, Writable {
  public let name: String
  public let age: Int32
  
  public static func read(from inp: InputPort, using buf: inout Data) -> Person {
    return Person(
      name: String.read(from: inp, using: &buf),
      age: Int32.read(from: inp, using: &buf)
    )
  }
  
  public func write(to out: OutputPort) {
    name.write(to: out)
    age.write(to: out)
  }
}

角色 2:运行时序列化库

private/serde.rkt 提供 Racket 端的序列化实现:

(define (read-record info [in (current-input-port)])
  (apply
   (record-info-constructor info)
   (for/list ([f (in-list (record-info-fields info))])
     (read-field (record-field-type f) in))))

(define (write-record info v [out (current-output-port)])
  (for ([f (in-list (record-info-fields info))])
    (define type (record-field-type f))
    (define value ((record-field-accessor f) v))
    (write-field type value out)))

为什么两端都需要序列化?

因为序列化是双向的

Swift → Racket:
Swift Person → Writable.write() → 字节流 → Racket read-record() → Racket Person

Racket → Swift:
Racket Person → write-record() → 字节流 → Swift Readable.read() → Swift Person

运行时加载

noise-serde-lib 的 Racket 代码会被编译成 .zo 文件,运行时加载:

racket.load(zo: URL(fileURLWithPath: "/path/to/noise-serde.zo"))
let serde = racket.require("serde", from: "module")

关键理解:noise-serde-lib 不在静态库中,而是作为编译后的 Racket 代码(.zo)被加载到 Racket VM 中执行。

七、完整的集成示例

开发阶段

;; 定义数据结构
#lang racket
(require noise-serde-lib)

(define-record user
  [id : Varint]
  [name : String]
  [email : Optional String]
  [scores : Listof Float32])

(define-enum role
  [admin]
  [user]
  [guest])

运行代码生成器:

racket -l noise/codegen -t definitions.rkt > NoiseModels.swift

生成的 Swift 代码:

public struct User: Readable, Writable {
  public let id: UVarint
  public let name: String
  public let email: String?
  public let scores: [Float32]
}

public enum Role: Readable, Writable {
  case admin
  case user
  case guest
}

运行阶段

import Noise
import NoiseSerde
import NoiseBackend

// 初始化 Racket VM
let racket = Racket()

// 加载 noise-serde 运行时
racket.load(zo: URL(fileURLWithPath: Bundle.module.path(forResource: "noise-serde", ofType: "zo")!))

// 加载你的 Racket 代码
racket.load(zo: URL(fileURLWithPath: Bundle.module.path(forResource: "my-app", ofType: "zo")!))

// 创建一个 Swift 对象
let user = User(id: 123, name: "Alice", email: "alice@example.com", scores: [0.9, 0.8, 0.95])

// 序列化为字节数据
var buffer = Data()
let outputPort = DataOutputPort(buffer: &buffer)
user.write(to: outputPort)

// 在 Racket 端反序列化(通过 RPC)
let result = backend.call("process-user", with: user)

// Racket 返回的结果自动反序列化为 Swift 类型
let processed: User = result.get()

八、总结

Noise 的集成可以概括为以下几个核心要点:

文件组成

文件类型 作用 谁提供
静态库 (.a) Racket VM 的机器码 Racket CS 构建
头文件 (.h) C 函数接口声明 Racket CS + Noise 封装
Boot 文件 (.boot) Racket VM 启动镜像 Racket CS 构建
.zo 文件 Racket 编译后的字节码 你的 Racket 代码 + noise-serde-lib

编译时流程

Racket CS 源码 → .a → .xcframework
                        ↓
                Swift Package Manager
                        ↓
Noise Swift 代码 ← 头文件 ← .xcframework
                        ↓
              链接生成 Noise.framework

运行时流程

Swift 应用启动
    ↓
Racket.init() → racket_boot()
    ↓
读取 boot 文件 → 初始化 Racket VM
    ↓
加载 .zo 文件 → 注册 Racket 模块
    ↓
Swift ↔ Racket 通信

关键理解

  1. Noise 不是一个静态链接库,而是一个 Swift 库,它依赖 RacketCS.xcframework
  2. RacketCS.xcframework 只包含 VM,不包含业务逻辑
  3. Boot 文件由 Swift 代码加载,在运行时初始化 Racket VM
  4. 头文件被 Swift 编译器使用,生成调用 C 函数的代码
  5. noise-serde-lib 是双重角色:编译时生成 Swift 代码,运行时提供 Racket 序列化逻辑

Noise 的设计体现了语言的互操作性之美,通过精心设计的分层架构,让 Swift 和 Racket 能够无缝协作。希望本文能够帮助你更好地理解这个复杂但优雅的系统。